/*
 * @Description: 
 * @Author: wangqi
 * @Date: 2021-04-19 21:02:24
 * @LastEditTime: 2021-04-23 11:12:39
 */

const fs = require('fs')
const path = require('path')
const ejs = require('ejs')

//babelon 把源码解析成AST
const babylon = require('babylon')
//@babel/types 节点替换
const types = require('@babel/types')
//@babel/traverse 遍历节点
const traverse = require('@babel/traverse').default
//@babel/generator 生成
const generator = require('@babel/generator').default

class Compiler {
    constructor(config) {
        this.config = config;
        // 入口文件的路径
        this.entryId;
        // 保存所有的模块依赖
        this.modules = {};
        // 入口路径
        this.entry = config.entry;
        // 输出文件
        this.assets = {};
        // 当前命令行的工作路径
        this.root = process.cwd();
    }

    getSource(modulePath) {
        // ./src/main.js zhu
        let content = fs.readFileSync(modulePath, 'utf-8')
        let rules = this.config.module.rules
        rules.forEach(rule => {
            let { test, use } = rule
            let len = use.length - 1
            if (test.test(modulePath)) {
                (function normalLoader() {
                    //找到实际项目中的loader
                    let loader = require(use[len--])
                    content = loader(content)
                    if (len >= 0) {
                        normalLoader()
                    }
                })();
            }
        });

        return content
    }

    // 构建模块
    buildModule(modulePath, isEntry) {
        // 模块内容  入口文件内容
        let source = this.getSource(modulePath)
      
        // 模块id   入口文件路径 ./src/main.js  
        let moduleName = ("./" + path.relative(this.root, modulePath)).split(path.sep).join("/")
        // let moduleName = "./" + path.relative(this.root, modulePath)
        // 如果是主入口
        if (isEntry) { this.entryId = moduleName };

        // 解析source源码进行改造, 返回一个依赖列表
        // 1. require 游览器不识别, 需要手动写个polyfill; 类似 __webpack_require__
        // 2. dist源码入口文件中的路径key, 都是在当前运行cmd命令目录位置生成的, 入口main.js中 ./a.js; 但实际打包时是需要相对 cmd目录 || webpack.config.js而言, 所以要改成 ./src/a.js        
        let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))

        // 把入口文件相对路径和模块中的内容对应起来
        this.modules[moduleName] = sourceCode;

        // 递归把所有嵌套依赖项对应起来
        dependencies.forEach(dep => this.buildModule(dep, false))
    }

    // 解析源码; AST解析语法树
    parse(source, parentPath) {
        let ast = babylon.parse(source)
        let dependencies = []//数组依赖
        // 遍历ast, 在遍历的过程中可对节点信息进行修改，生成新的 AST
        traverse(ast, {
            // 调用表达式  a执行  require执行
            CallExpression(p) {
                let node = p.node //对应的节点
                if (node.callee.name === 'require') {
                    node.callee.name = '__webpack_require__'
                    let moduleName = node.arguments[0].value
                    moduleName = moduleName + (path.extname(moduleName) ? '' : '.js')
                    moduleName = ('./' + path.join(parentPath, moduleName)).split(path.sep).join("/")
                    dependencies.push(moduleName)
                    //节点替换
                    node.arguments = [types.StringLiteral(moduleName)]
                }
            }
        });
        let sourceCode = generator(ast).code;

        return { sourceCode, dependencies }
    }

    // 发射打包文件
    emitFile() {
        // 输入路径
        let build = path.join(this.config.output.path, this.config.output.filename)
        // 读取模板
        let templateStr = this.getSource(path.join(__dirname, 'build.ejs'))
        // 渲染模板
        let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules })
        this.assets[build] = code
        fs.writeFileSync(build, this.assets[build])
    }

    run() {
        // 根据入口文件解析依赖变成打包生产的路径对象
        let entry = path.resolve(this.root, this.entry)
        this.buildModule(entry, true)
        // 发射一个打包后的文件
        this.emitFile()
    }
}

module.exports = Compiler;

